﻿#region License, Terms and Author(s)
//
// ELMAH - Error Logging Modules and Handlers for ASP.NET
// Copyright (c) 2004-12 Atif Aziz. All rights reserved.
//
//  Author(s):
//
//      James Driscoll, mailto:jamesdriscoll@btinternet.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion

[assembly: Elmah.Scc("$Id: $")]

namespace Elmah.Analysis
{
    #region Imports
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Xml.Linq;
    #endregion

    /// <summary>
    /// Analyzes ELMAH's configuration
    /// Makes recommendations of what to change where appropriate
    /// Lists out current settings for the different modules
    /// </summary>
    public class ConfigurationAnalysis
    {
        public ConfigurationAnalysis(HttpContextBase context)
        {
            Context = context;
            Results = new List<ConfigurationAnalysisItem>();
        }

        internal List<ConfigurationAnalysisItem> Results { get; private set; }
        internal bool IsHandlerConfiguredSecurely { get; private set; }

        private void LogResult(string name, string setting, string advice, string description)
        {
            var configurationAnalysisItem = new ConfigurationAnalysisItem
                                                {
                                                    Name = name,
                                                    Setting = setting,
                                                    Advice = advice,
                                                    Description = description
                                                };

            Results.Add(configurationAnalysisItem);
        }

        private void LogInsecureResult(string name, string setting, string advice, string description)
        {
            LogResult(name, setting, advice, description);
            IsHandlerConfiguredSecurely = false;
        }

        private void LogRunningModuleResult(Type moduleType)
        {
            LogResult(moduleType.Name, null, null, string.Format("The {0} is currently running", moduleType.Name));
        }

        private HttpContextBase Context { get; set; }
        private XDocument WebConfigDocument { get; set; }

        public void Run()
        {
            Results.Clear();
            IsHandlerConfiguredSecurely = true;

            if (!LoadWebConfig())
                return;

            CheckSecurity();
            CheckHandler();
            CheckModules();
        }

        private bool LoadWebConfig()
        {
            try
            {
                var webConfigFileName = Context.Server.MapPath("~/web.config");
                WebConfigDocument = XDocument.Load(webConfigFileName);

                if (WebConfigDocument.Root == null)
                    LogResult("web.config", null, null, "web.config appears to be empty, so analysis is not possible");

                return WebConfigDocument.Root != null;
            }
            catch (Exception ex)
            {
                LogResult("web.config", null, "Review this exception message: " + ex.Message,
                          "web.config could not be loaded, so analysis is not possible");
                return false;
            }
        }

        public void CheckHandler()
        {
            AnalyzeHandlers("system.web", "httpHandlers", true);
            AnalyzeHandlers("system.webServer", "handlers", false);
        }

        private void AnalyzeHandlers(string systemWebElementName, string handlerElementName,
                                     bool analyzeLocationAuthorization)
        {
            Debug.Assert(WebConfigDocument.Root != null);

            var root = WebConfigDocument.Root.ToIEnumberable();
            var locations = WebConfigDocument
                .Root
                .Elements("location");

            var rootHandlers = GetHandlerEntries(root, systemWebElementName, handlerElementName);
            var locationHandlers = GetHandlerEntries(locations, systemWebElementName, handlerElementName);

            var handlerCount = 0;

            foreach (var handler in rootHandlers)
            {
                handlerCount++;
                var path = handler.GetSafeAttributeValue("path");
                LogInsecureResult(path, systemWebElementName + "/" + handlerElementName,
                                  "Move the handler into a <location path=\"elmah.axd\" /> element",
                                  "The handler is VERY likely to be insecure unless it is locked down in a <location /> element");
            }

            foreach (var handler in locationHandlers)
            {
                handlerCount++;
                var verb = handler.GetSafeAttributeValue("verb");
                if (!verb.SplitToTrimmedUpperInvariant().Contains("GET"))
                    LogResult("Handler GET", "<add verb=\"GET\" ... />",
                              "Add GET to the list of verbs (ideally verb=\"GET, POST, HEAD\")",
                              "The handler needs to have GET in the list of allowed verbs");
                var handlerPath = handler.GetSafeAttributeValue("path");

                Debug.Assert(handler.Parent != null);
                Debug.Assert(handler.Parent.Parent != null);
                Debug.Assert(handler.Parent.Parent.Parent != null);

                var location = handler
                    .Parent // httpHandlers or handlers
                    .Parent // system.web or system.webServer
                    .Parent; // location
                var locationPath = location.GetSafeAttributeValue("path");

                if (handlerPath != locationPath)
                {
                    var setting = string.Format(
                        "<location path=\"{0}\"><{1}><{2}><add path=\"{3}\" ... /></{2}></{1}>", locationPath,
                        systemWebElementName, handlerElementName, handlerPath);
                    LogInsecureResult(handlerPath, setting,
                                      "Change the handler path to be the same as the location path",
                                      "The handler is only secure if it's path is the same as the location path");
                }
                if (analyzeLocationAuthorization)
                    AnalyzeLocationAuthorization(location);
            }

            if (handlerCount == 0)
            {
                var setting =
                    string.Format("<location path=\"elmah.axd\"><{0}><{1}><add path=\"elmah.axd\" ... /></{1}></{0}>",
                                  systemWebElementName, handlerElementName);

                LogResult("elmah.axd", setting, "No handler could be found",
                          "This might be OK if you do not want anyone to view the error log from this application, the handler is declared at machine.config level, or you are using ASP.NET MVC and have provide indirection through MVC to reach ELMAH");
            }

            if (handlerCount > 1)
            {
                var setting = string.Format("<{0}><{1}><add ... /></{1}></{0}>", systemWebElementName,
                                            handlerElementName);
                LogInsecureResult("elmah.axd", setting,
                                  "Remove any additional handler declarations in this section",
                                  "More than one handler found. This is a security risk. You should only declare the handler once!");
            }
        }

        private void AnalyzeLocationAuthorization(XElement location)
        {
            var authorizationEntries = location
                .Elements("system.web")
                .Elements("authorization")
                .Elements();

            var allowedUsers = new HashSet<string>();
            var deniedUsers = new HashSet<string>();
            var allowedRoles = new HashSet<string>();
            var deniedRoles = new HashSet<string>();

            foreach (var authorizationEntry in authorizationEntries)
            {
                if (authorizationEntry.Name == "allow")
                {
                    if (ProcessAllowedEntries(authorizationEntry, "users", allowedUsers, deniedUsers, true))
                        break;

                    ProcessAllowedEntries(authorizationEntry, "roles", allowedRoles, deniedRoles, false);
                }

                if (authorizationEntry.Name != "deny")
                    continue;

                if (ProcessDeniedEntries(authorizationEntry, "users", deniedUsers, true))
                    break;

                ProcessDeniedEntries(authorizationEntry, "roles", deniedRoles, false);
            }

            if (deniedUsers.Contains("*"))
            {
                if (allowedUsers.Count == 0 && allowedRoles.Count == 0)
                    LogResult("Deny access to all users", "<deny users=\"*\" />",
                              "Allow at least one specific user or role access to view the error log",
                              "The ELMAH handler has been declared, but all access to it has been denied. You probably want to allow specific users and/or roles to have access");
            }
            else
            {
                LogInsecureResult("Lack of <deny users=\"*\" />", "<deny users=\"*\" />",
                                  "Secure the error log immediately by adding authorization rules only allow specific users and/or roles to view the error log.",
                                  "It is likely that some/all users will be able to view the error log");
            }

            LogAllowedAccess(allowedUsers, "user");
            LogAllowedAccess(allowedRoles, "role");

            if (allowedUsers.Count == 0 && deniedUsers.Count == 0 && allowedRoles.Count == 0 && deniedRoles.Count == 0)
            {
                LogInsecureResult("No authorization allows access to all users",
                                  "<allow users=\"*\" />",
                                  "Change the authorization immediately so that only specific users or roles have access to the error log",
                                  "Any user of the website can view the error log. This is extremely insecure.");
            }
        }

        private void LogAllowedAccess(HashSet<string> allowed, string authorizationType)
        {
            if (allowed.Count == 0)
                return;

            LogResult(string.Format("Which {0}(s) have access to view the error log", authorizationType),
                      string.Format("<allow {0}s=\"\" />", authorizationType),
                      string.Format("Ensure that these {0}(s) should have access", authorizationType),
                      string.Format("The following {0}(s) have been allowed access to view the error log: {1}",
                                    authorizationType, string.Join(", ", allowed.ToArray())));
        }

        private bool ProcessAllowedEntries(XElement authorizationEntry, string authorizationType,
                                           HashSet<string> allowed, HashSet<string> denied, bool checkForWildcards)
        {
            var authorizationDetails = GetAuthorizationDetails(authorizationEntry, authorizationType)
                                            .Where(item => !denied.Contains(item));
            foreach (var item in authorizationDetails)
            {
                allowed.Add(item);

                if (!checkForWildcards)
                    continue;

                if (item == "?")
                    LogInsecureResult("Allow access to any authenticated user",
                                      "<allow users=\"?\" />",
                                      "Only allow specfic users or roles to view the error log. If that is not possible, you must be sure that ANY authenticated user can view the error log",
                                      "It is preferable to only allow specific users and/or roles to view the error log, then to deny access to everyone else with <deny users\"*\" />");

                if (item != "*")
                    continue;

                LogInsecureResult("Allow access to all users",
                                  "<allow users=\"*\" />",
                                  "Change the authorization immediately so that only specific users or roles have access to the error log",
                                  "Any user of the website can view the error log. This is extremely insecure.");
                return true;
            }

            return false;
        }

        private static bool ProcessDeniedEntries(XElement authorizationEntry, string authorizationType, HashSet<string> denied,
                                          bool checkForWildcards)
        {
            foreach (var item in GetAuthorizationDetails(authorizationEntry, authorizationType))
            {
                denied.Add(item);

                if (!checkForWildcards)
                    continue;

                if (item == "*")
                    return true;
            }

            return false;
        }

        private static IEnumerable<string> GetAuthorizationDetails(XElement authorizationEntry, string authorizationType)
        {
            return authorizationEntry.GetSafeAttributeValue(authorizationType).SplitToTrimmedUpperInvariant();
        }

        private static IEnumerable<XElement> GetHandlerEntries(IEnumerable<XElement> elements,
                                                               string systemWebElementName, string handlerElementName)
        {
            return elements
                .Elements(systemWebElementName)
                .Elements(handlerElementName)
                .Elements("add")
                .Where(e =>
                       e.GetSafeAttributeValue("type").StartsWith("Elmah.ErrorLogPageFactory") &&
                       !e.ElementsAfterSelf("clear").Any());
        }

        public void CheckSecurity()
        {
            var allowRemoteAccess = SecurityConfiguration.Default.AllowRemoteAccess;
            LogResult("Allow Remote Access",
                      "<elmah><security allowRemoteAccess=\"true|false\" /></elmah>",
                      allowRemoteAccess
                          ? "Remote access is currently allowed - you should normally only do this if you do not have RDP access to the server."
                          : "Remote access is prevented - this is usually a good thing! Only set this to true if you do not have RDP acccess to the server",
                      "When set to true will allow remote access to elmah.axd"
                );
        }

        public void CheckModules()
        {
            var application = Context.ApplicationInstance;

            foreach (var module in HttpModuleRegistry.GetModules(application))
            {
                var moduleType = module.GetType();
                AnalyzeErrorFilterModule(moduleType);
                AnalyzeErrorLogModule(moduleType);
                AnalyzeErrorMailModule(moduleType);
                AnalyzeErrorTweetModule(moduleType);
                AnalyzeFixIIS5xWildcardMappingModule(moduleType);
                AnalyzeMsAjaxDeltaErrorLogModule(moduleType);
            }
        }

        private void AnalyzeErrorFilterModule(Type moduleType)
        {
            var type = typeof(ErrorFilterModule);
            if (moduleType != type)
                return;

            LogRunningModuleResult(moduleType);
            // TODO - add further analysis
            // possibly understand what the filter is trying to do
            // look for incorrect usage of <errorFilter><or /><and /></errorFilter> etc
        }

        private void AnalyzeErrorLogModule(Type moduleType)
        {
            var type = typeof(ErrorLogModule);
            if (moduleType != type)
                return;

            LogRunningModuleResult(moduleType);
            // TODO - add further analysis
            // work out which log we are using
            // advise to use named connection strings instead of in-place connection strings
            // possible check db connection where appropriate
            // possibly look at db schemas to check they are ok
        }

        private void AnalyzeErrorMailModule(Type moduleType)
        {
            var type = typeof(ErrorMailModule);
            if (moduleType != type)
                return;

            LogRunningModuleResult(moduleType);
            // TODO - add further analysis
            // list email details
            // work out if the email is being sent via <elmah> settings or .net settings
        }

        private void AnalyzeErrorTweetModule(Type moduleType)
        {
            var type = typeof(ErrorTweetModule);
            if (moduleType != type)
                return;

            LogRunningModuleResult(moduleType);
            // TODO - add further analysis
            // list current tweet settings
        }

        private void AnalyzeFixIIS5xWildcardMappingModule(Type moduleType)
        {
            var type = typeof(FixIIS5xWildcardMappingModule);
            if (moduleType != type)
                return;

            LogResult(type.Name, "Module declarations", "Remove this module declaration unless you are 100% certain you require it", "You will only require this module is you are using wildcard mapping in IIS5.x");
        }

        private void AnalyzeMsAjaxDeltaErrorLogModule(Type moduleType)
        {
            var type = typeof (MsAjaxDeltaErrorLogModule);
            if (moduleType != type)
                return;

            LogResult(type.Name, "Module declarations", "Remove this module declaration unless you are 100% certain you require it", "The MS Ajax Delta Error Log should not be required any more as it MS Ajax has been patched such that the module is no longer required");
        }
    }
}
